﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;

namespace GSdk.Net.Lglcd
{
    using InteropServices;

    /// <summary>
    /// You can perform update operations and effectively change you applet graphics on the display through this class.
    /// It will handles all open/close device operations for you, the important thing is to call Dispose on the applet.
    /// </summary>
    public abstract class Device : IDisposable
    {
        protected bool DeviceOpened { get; private set; }
        private lgLcdOnSoftButtonsCB m_SoftButtonsHandler;
        private volatile DevicesButtons m_LastSoftButtons = DevicesButtons.None;
        private uint m_LastSoftButtonsState = 0U;
        private DeviceState m_LastDeviceState = new DeviceState();
        private bool m_ForegroundApplet;
        internal lgLcdOpenByTypeContext m_OpenByTypeContext;
        internal lgLcdBitmap m_DeviceBitmap;
        protected Applet Parent { get; private set; }
        
        /// <summary>
        /// This the type of this device, don't use Or'd devices because it will throws an exception when instance is created.
        /// </summary>
        public abstract DeviceTypes DeviceType { get; }

        /// <summary>
        /// This is the update way used by the device, see lglcd.pdf for more details.
        /// </summary>
        public UpdateStyles UpdateStyle { get; set; }

        /// <summary>
        /// Display priority of the applet
        /// </summary>
        public AppletPriorities AppletPriority { get; set; }

        public bool Disposed { get; private set; }

        /// <summary>
        /// Set the applet as foreground (it will stay on top of all the others).
        /// </summary>
        public bool ForegroundApplet
        {
            get { return m_ForegroundApplet; }
            set
            {
                EnsureDeviceIsWorking();

                if (m_ForegroundApplet != value)
                {
                    InteropMethodsWrapper.SetAsForegroundApplet(this, value);
                    m_ForegroundApplet = value;
                }
            }
        }

        /// <summary>
        /// This will try to open the device, if it's not already opened.
        /// It will return false only if the device can't be opened and it's not already opened
        /// </summary>
        public bool HasDevice
        {
            get
            {
                try
                {
                    EnsureDeviceIsWorking();
                }
                catch
                {
                    return false;
                }

                return true;
            }
        }

        /// <summary>
        /// This method must be called to update device graphics.
        /// If possible, use Update(Bitmap) overload that makes things easier.
        /// </summary>
        /// <param name="image">Byte array that must respect sizes specified in lglcd.pdf</param>
        /// <returns>false if device is not connected. It will throw exceptions if other errors occur.</returns>
        public bool Update(byte[] image)
        {
            IntPtr bitmapStruct;
            EnsureDeviceIsWorking();

            switch (DeviceType)
            {
                case DeviceTypes.BlackAndWhite:
                    m_DeviceBitmap.bmp_mono.hdr.Format = Formats.LGLCD_BMP_FORMAT_160x43x1;
                    m_DeviceBitmap.bmp_mono.Pixels = image;
                    bitmapStruct = m_DeviceBitmap.bmp_mono.Unmanaged;
                    break;
                case DeviceTypes.Qvga:
                    m_DeviceBitmap.bmp_qvga32.hdr.Format = Formats.LGLCD_BMP_FORMAT_QVGAx32;
                    m_DeviceBitmap.bmp_qvga32.Pixels = image;
                    bitmapStruct = m_DeviceBitmap.bmp_qvga32.Unmanaged;
                    break;
                default:
                    throw new DeviceException("Invalid device type");
            }

            m_OpenByTypeContext.connection = Parent.m_ConnectionContext.connection;
            return InteropMethodsWrapper.Update(this, bitmapStruct);
        }

        /// <summary>
        /// This method uses a custom algorithm to effectively convert a bitmap image into a byte array.
        /// The algorithm is focused on reducing CPU consumption, not on speed (and that's why it's not multithreaded, which was my initial thought).
        /// The method could be overridden: provide your own algorithm and call Update(byte[]) to change this.
        /// </summary>
        /// <param name="image"></param>
        /// <returns>Same values of Update(byte[])</returns>
        public virtual bool Update(Bitmap image)
        {
            IntPtr bitmapStruct;
            EnsureDeviceIsWorking();

            switch (DeviceType)
            {
                case DeviceTypes.BlackAndWhite:
                    m_DeviceBitmap.bmp_mono.hdr.Format = Formats.LGLCD_BMP_FORMAT_160x43x1;
                    m_DeviceBitmap.bmp_mono.SetPixels(image);
                    bitmapStruct = m_DeviceBitmap.bmp_mono.Unmanaged;
                    break;
                case DeviceTypes.Qvga:
                    m_DeviceBitmap.bmp_qvga32.hdr.Format = Formats.LGLCD_BMP_FORMAT_QVGAx32;
                    m_DeviceBitmap.bmp_qvga32.SetPixels(image);
                    bitmapStruct = m_DeviceBitmap.bmp_qvga32.Unmanaged;
                    break;
                default:
                    throw new DeviceException("Invalid device type");
            }

            m_OpenByTypeContext.connection = Parent.m_ConnectionContext.connection;
            return InteropMethodsWrapper.Update(this, bitmapStruct);
        }

        /// <summary>
        /// Return the buttons state of the device, it uses a sort of small cache to reduce memory consumption.
        /// </summary>
        /// <returns></returns>
        public DeviceState GetState()
        {
            EnsureDeviceIsWorking();

            uint tmp = InteropMethodsWrapper.ReadSoftButtons(this);
            if (tmp != m_LastSoftButtonsState)
            {
                m_LastSoftButtonsState = tmp;
                m_LastDeviceState = new DeviceState(m_LastSoftButtonsState);
            }
            return m_LastDeviceState;
        }

        private uint SoftButtonsHandler([In] int device, [In] uint dwButtons, [In] IntPtr pContext)
        {
            DevicesButtons buttons = (DevicesButtons)dwButtons;
            OnButtonsDown(buttons &~ m_LastSoftButtons);
            OnButtonsUp(m_LastSoftButtons & ~buttons);

            m_LastSoftButtons = buttons;
            return 0U;
        }

        protected internal void OnDeviceArrival()
        {
            if (DeviceArrival != null) DeviceArrival(this, EventArgs.Empty);
        }

        protected internal void OnDeviceRemoval()
        {
            if (DeviceRemoval != null) DeviceRemoval(this, EventArgs.Empty);
        }

        protected void OnButtonsDown(DevicesButtons buttons)
        {
            if (ButtonsDown != null) ButtonsDown(this, new DevicesButtonsEventArgs(buttons));
        }

        protected void OnButtonsUp(DevicesButtons buttons)
        {
            if (ButtonsUp != null) ButtonsUp(this, new DevicesButtonsEventArgs(buttons));
        }

        /// <summary>
        /// Event fired when device is USB-Connected (if it was disconnected)
        /// </summary>
        public event EventHandler DeviceArrival;
        /// <summary>
        /// Event fired when device is USB-Disconnected (if it was connected)
        /// </summary>
        public event EventHandler DeviceRemoval;
        /// <summary>
        /// Event fired when button is pressed
        /// </summary>
        public event EventHandler<DevicesButtonsEventArgs> ButtonsDown;
        /// <summary>
        /// Event fired when button is released
        /// </summary>
        public event EventHandler<DevicesButtonsEventArgs> ButtonsUp;

        private void OpenDevice()
        {
            if (DeviceOpened) return;

            InteropMethodsWrapper.Open(this);

            DeviceOpened = true;
        }

        private void CloseDevice()
        {
            InteropMethodsWrapper.Close(this);
            m_OpenByTypeContext.device = (int)DescriptorErrors.LGLCD_INVALID_DEVICE;

            DeviceOpened = false;
        }

        private void EnsureDeviceIsWorking()
        {
            if (Disposed)
                throw new ObjectDisposedException("Device: " + DeviceType.ToString());

            OpenDevice();
        }

        public Device(Applet parent)
        {
            UpdateStyle = UpdateStyles.Async;
            AppletPriority = AppletPriorities.Normal;

            m_SoftButtonsHandler = new lgLcdOnSoftButtonsCB(SoftButtonsHandler);
            bool hasAdeviceType = false;
            if (parent == null)
                throw new ArgumentNullException();

            foreach (DeviceTypes value in Enum.GetValues(DeviceType.GetType()))
                if (value != DeviceTypes.Invalid)
                    if (DeviceType.HasFlag(value))
                        if (!hasAdeviceType)
                            hasAdeviceType = true;
                        else
                            throw new DeviceException("A device can only have one DeviceType");

            Parent = parent;

            m_OpenByTypeContext.connection = Parent.m_ConnectionContext.connection;
            m_OpenByTypeContext.device = (int)DescriptorErrors.LGLCD_INVALID_DEVICE;
            
            switch (DeviceType)
            {
                case DeviceTypes.BlackAndWhite:
                    m_OpenByTypeContext.deviceType = InteropServices.DeviceTypes.LGLCD_DEVICE_BW;
                    break;
                case DeviceTypes.Qvga:
                    m_OpenByTypeContext.deviceType = InteropServices.DeviceTypes.LGLCD_DEVICE_QVGA;
                    break;
            }
            m_OpenByTypeContext.onSoftbuttonsChanged.softbuttonsChangedCallback = m_SoftButtonsHandler;
        }

        #region Dispose handling
        /// <summary>
        /// Automatically called by Applet Dispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (Disposed) return;

            if (disposing)
            {
                // Managed resources
            }

            // Unmanaged resources
            try
            {
                CloseDevice();
            }
            catch { }

            switch (DeviceType)
            {
                case DeviceTypes.BlackAndWhite:
                    m_DeviceBitmap.bmp_mono.Dispose();
                    break;
                case DeviceTypes.Qvga:
                    m_DeviceBitmap.bmp_qvga32.Dispose();
                    break;
            }

            Disposed = true;
        }

        ~Device()
        {
            Dispose(false);
        }
        #endregion
    }
}
